﻿
using Boilen.Primitives;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;


namespace BoilenEditor.Primitives.Snippets {

    [DebuggerDisplay( "DeclarationData: FullName={FullName}" )]
    public partial class DeclarationData {

        private static readonly Dictionary<Type, IEnumerable<SetMemberGroupData>> ConfigurationSnippetMemberCache = new Dictionary<Type, IEnumerable<SetMemberGroupData>>( );


        public string GetDeclarationText( bool includeDescription ) {
            return this.AppendTo( new StringBuilder( ), includeDescription )
                .ToString( );
        }

        public StringBuilder AppendTo( StringBuilder sb, bool includeDescription ) {
            sb.Append( Ex.TabString )
              .Append( '.' )
              .Append( this.Method.Name );

            // Type Argument
            ArgumentData firstArg = this.Arguments.FirstOrDefault( );
            bool isTypeArg = firstArg.HasValue( ) && firstArg.Name == "T";
            if( isTypeArg )
                sb.Append( '<' )
                  .Append( firstArg.Value )
                  .Append( '>' );

            sb.Append( '(' );

            // Arguments
            int argStart = isTypeArg ? 1 : 0;
            for( int i = argStart; i < this.Arguments.Count; ++i ) {
                ArgumentData arg = this.Arguments[i];
                if( !includeDescription && arg.IsDescription )
                    continue;

                if( i > argStart )
                    sb.Append( ',' );
                sb.Append( ' ' );

                arg.AppendTo( sb );
            }

            // Configuration Members
            if( this.Members.Any( m => m.IsSet ) ) {
                if( this.Arguments.Count != argStart )
                    sb.Append( ',' );
                sb.AppendLine( " p => p" );
                this.Members.ForEach( m => m.AppendTo( sb ) );
                sb.Append( Ex.TabString );
            }
            else {
                sb.Append( ' ' );
            }

            sb.AppendLine( ")" );

            return sb;
        }


        private static string GetFullName( MethodInfo method ) {
            var sb = new StringBuilder( method.Name );

            if( method.IsGenericMethod )
                sb.Append( '<' )
                  .Append( string.Join( ",", method.GetGenericArguments( ).Select( a => a.Name ).ToArray( ) ) )
                  .Append( '>' );

            sb.Append( "(" );
            sb.Append( string.Join( ",", method.GetParameters( ).Select( p => " " + p.ParameterType.GetTypeName( ) + " " + p.Name ).ToArray( ) ) );
            sb.Append( " )" );

            return sb.ToString( );
        }

        private static ReadOnlyCollection<ArgumentData> GetArguments( MethodInfo method ) {
            var formatAttribute = method.GetCustomAttribute<DescriptionAttribute>( );
            string format = formatAttribute.GetValueOrDefault( fa => "({0}{1}{0})".With( '"', fa.Description.Replace( "{0}", "___" ) ), "" );

            var typeArgumentMembers = method.IsGenericMethod
                ? method.GetGenericArguments( )
                        .Select( t => new ArgumentData( t.Name, typeof( Type ), "" ) )
                : Enumerable.Empty<ArgumentData>( );

            var parameterMembers =
                from p in method.GetParameters( )
                where !GetConfigurationParameterType( p ).HasValue( )
                select new ArgumentData( Util.Capitalize( p.Name ), p.ParameterType, p.Name == "description" ? format : "" );

            var members = typeArgumentMembers
                .Concat( parameterMembers )
                .ToArray( );
            return members.ToReadOnlyCollection( );
        }

        private static ReadOnlyCollection<SetMemberGroupData> GetMembers( MethodInfo method ) {
            var parameters = method.GetParameters( );
            var configurationType = GetConfigurationParameterType( parameters.LastOrDefault( ) );
            var configurationMembers =
                Ex.Enumerate( configurationType, t => t.HasValue( ), t => t.BaseType )
                  .SelectMany( type => GetConfigurationSnippetMembers( type ) )
                  .ToArray( );

            return configurationMembers.ToReadOnlyCollection( );
        }

        private static Type GetConfigurationParameterType( ParameterInfo parameter ) {
            if( parameter.HasValue( ) ) {
                Type parameterType = parameter.ParameterType;
                bool isConfigurationParameter = parameterType.IsGenericType
                    && parameterType.GetGenericTypeDefinition( ) == typeof( Action<> );
                if( isConfigurationParameter )
                    return parameterType.GetGenericArguments( ).Single( );
            }

            return null;
        }

        private static IEnumerable<SetMemberGroupData> GetConfigurationSnippetMembers( Type type ) {
            IEnumerable<SetMemberGroupData> members;
            if( !ConfigurationSnippetMemberCache.TryGetValue( type, out members ) ) {
                var configurationMembers =
                    from m in type.GetMethods( BindingFlags.Public | BindingFlags.Instance )
                    let parameterType = GetSetMemberParameterType( type, m )
                    where parameterType.HasValue( )
                    let name = m.Name.Substring( 3 )
                    let category = m.GetCustomAttribute<CategoryAttribute>( ).GetValueOrDefault( a => a.Category, name )
                    let member = new SetMemberData( name, parameterType, m )
                    group member by category into g
                    select new SetMemberGroupData( g );

                members = ConfigurationSnippetMemberCache[type] = configurationMembers.ToReadOnlyCollection( );
            }

            return members;
        }

        private static Type GetSetMemberParameterType( Type declaringType, MethodInfo method ) {
            bool isSetMethod =
                   method.DeclaringType == declaringType
                && method.ReturnType == declaringType
                && method.Name.StartsWith( "Set" )
                && method.GetCustomAttribute<EditorBrowsableAttribute>( )
                    .GetValueOrDefault( a => a.State, EditorBrowsableState.Always ) == EditorBrowsableState.Always;

            if( isSetMethod ) {
                var parameter = method.GetParameters( ).SingleOrDefault( );
                Type parameterType = parameter.GetValueOrDefault( p => p.ParameterType, null );
                if( !parameterType.GetCustomAttribute<FlagsAttribute>( ).HasValue( )
                  || method.GetCustomAttribute<DefaultValueAttribute>( ).HasValue( ) )
                    return parameterType;
            }

            return null;
        }

    }

}
